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