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