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