image.go (view raw)
1package captcha
2
3import (
4 "image"
5 "image/png"
6 "io"
7 "os"
8 "rand"
9)
10
11const (
12 // Standard width and height for captcha image
13 StdWidth = 300
14 StdHeight = 80
15
16 maxSkew = 2
17)
18
19type CaptchaImage struct {
20 *image.NRGBA
21 primaryColor image.NRGBAColor
22 numberWidth int
23 numberHeight int
24 dotSize int
25}
26
27// NewImage returns a new captcha image of the given width and height with the
28// given slice of numbers, where each number must be in range 0-9.
29func NewImage(numbers []byte, width, height int) *CaptchaImage {
30 img := new(CaptchaImage)
31 img.NRGBA = image.NewNRGBA(width, height)
32 img.primaryColor = image.NRGBAColor{uint8(rand.Intn(50)), uint8(rand.Intn(50)), uint8(rand.Intn(128)), 0xFF}
33 // We need some space, so calculate border
34 var border int
35 if width > height {
36 border = height / 5
37 } else {
38 border = width / 5
39 }
40 bwidth := width - border*2
41 bheight := height - border*2
42 img.calculateSizes(bwidth, bheight, len(numbers))
43 // Background
44 img.fillWithCircles(10, img.dotSize)
45 maxx := width - (img.numberWidth+img.dotSize)*len(numbers) - img.dotSize
46 maxy := height - img.numberHeight - img.dotSize*2
47 x := rnd(img.dotSize*2, maxx)
48 y := rnd(img.dotSize*2, maxy)
49 setRandomBrightness(&img.primaryColor, 180)
50 for _, n := range numbers {
51 img.drawNumber(font[n], x, y)
52 x += img.numberWidth + img.dotSize
53 }
54 img.strikeThrough()
55 return img
56}
57
58// NewRandomImage generates random numbers and returns a new captcha image of
59// the given width and height with those numbers printed on it, and the numbers
60// themselves.
61func NewRandomImage(width, height int) (img *CaptchaImage, numbers []byte) {
62 numbers = randomNumbers()
63 img = NewImage(numbers, width, height)
64 return
65}
66
67// PNGEncode writes captcha image in PNG format into the given writer.
68func (img *CaptchaImage) PNGEncode(w io.Writer) os.Error {
69 return png.Encode(w, img)
70}
71
72func (img *CaptchaImage) calculateSizes(width, height, ncount int) {
73 // Goal: fit all numbers into the image.
74 // Convert everything to floats for calculations.
75 w := float64(width)
76 h := float64(height)
77 // fontWidth includes 1-dot spacing between numbers
78 fw := float64(fontWidth) + 1
79 fh := float64(fontHeight)
80 nc := float64(ncount)
81 // Calculate width of a sigle number if we only take into
82 // account the width
83 nw := w / nc
84 // Calculate the number height from this width
85 nh := nw * fh / fw
86 // Number height too large?
87 if nh > h {
88 // Fit numbers based on height
89 nh = h
90 nw = fw / fh * nh
91 }
92 // Calculate dot size
93 img.dotSize = int(nh / fh)
94 // Save everything, making actual width smaller by 1 dot,
95 // to account for spacing between numbers
96 img.numberWidth = int(nw)
97 img.numberHeight = int(nh) - img.dotSize
98}
99
100func (img *CaptchaImage) drawHorizLine(color image.Color, fromX, toX, y int) {
101 for x := fromX; x <= toX; x++ {
102 img.Set(x, y, color)
103 }
104}
105
106func (img *CaptchaImage) drawCircle(color image.Color, x, y, radius int) {
107 f := 1 - radius
108 dfx := 1
109 dfy := -2 * radius
110 xx := 0
111 yy := radius
112
113 img.Set(x, y+radius, color)
114 img.Set(x, y-radius, color)
115 img.drawHorizLine(color, x-radius, x+radius, y)
116
117 for xx < yy {
118 if f >= 0 {
119 yy--
120 dfy += 2
121 f += dfy
122 }
123 xx++
124 dfx += 2
125 f += dfx
126 img.drawHorizLine(color, x-xx, x+xx, y+yy)
127 img.drawHorizLine(color, x-xx, x+xx, y-yy)
128 img.drawHorizLine(color, x-yy, x+yy, y+xx)
129 img.drawHorizLine(color, x-yy, x+yy, y-xx)
130 }
131}
132
133func min3(x, y, z uint8) (o uint8) {
134 o = x
135 if y < o {
136 o = y
137 }
138 if z < o {
139 o = z
140 }
141 return
142}
143
144func max3(x, y, z uint8) (o uint8) {
145 o = x
146 if y > o {
147 o = y
148 }
149 if z > o {
150 o = z
151 }
152 return
153}
154
155func setRandomBrightness(c *image.NRGBAColor, max uint8) {
156 minc := min3(c.R, c.G, c.B)
157 maxc := max3(c.R, c.G, c.B)
158 if maxc > max {
159 return
160 }
161 n := rand.Intn(int(max-maxc)) - int(minc)
162 c.R = uint8(int(c.R) + n)
163 c.G = uint8(int(c.G) + n)
164 c.B = uint8(int(c.B) + n)
165}
166
167func rnd(from, to int) int {
168 return rand.Intn(to+1-from) + from
169}
170
171func (img *CaptchaImage) fillWithCircles(n, maxradius int) {
172 color := img.primaryColor
173 maxx := img.Bounds().Max.X
174 maxy := img.Bounds().Max.Y
175 for i := 0; i < n; i++ {
176 setRandomBrightness(&color, 255)
177 r := rnd(1, maxradius)
178 img.drawCircle(color, rnd(r, maxx-r), rnd(r, maxy-r), r)
179 }
180}
181
182func (img *CaptchaImage) strikeThrough() {
183 r := 0
184 maxx := img.Bounds().Max.X
185 maxy := img.Bounds().Max.Y
186 y := rnd(maxy/3, maxy-maxy/3)
187 for x := 0; x < maxx; x += r {
188 r = rnd(1, img.dotSize/2-1)
189 y += rnd(-img.dotSize/2, img.dotSize/2)
190 if y <= 0 || y >= maxy {
191 y = rnd(maxy/3, maxy-maxy/3)
192 }
193 img.drawCircle(img.primaryColor, x, y, r)
194 }
195}
196
197func (img *CaptchaImage) drawNumber(number []byte, x, y int) {
198 skf := rand.Float64() * float64(rnd(-maxSkew, maxSkew))
199 xs := float64(x)
200 minr := img.dotSize / 2 // minumum radius
201 maxr := img.dotSize/2 + img.dotSize/4 // maximum radius
202 y += rnd(-minr, minr)
203 for yy := 0; yy < fontHeight; yy++ {
204 for xx := 0; xx < fontWidth; xx++ {
205 if number[yy*fontWidth+xx] != 1 {
206 continue
207 }
208 // introduce random variations
209 or := rnd(minr, maxr)
210 ox := x + (xx * img.dotSize) + rnd(0, or/2)
211 oy := y + (yy * img.dotSize) + rnd(0, or/2)
212 img.drawCircle(img.primaryColor, ox, oy, or)
213 }
214 xs += skf
215 x = int(xs)
216 }
217}