image.go (view raw)
1// Copyright 2011 Dmitry Chestnykh. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package captcha
6
7import (
8 "image"
9 "image/png"
10 "io"
11 "math"
12 "os"
13 "rand"
14)
15
16const (
17 // Standard width and height of a captcha image.
18 StdWidth = 240
19 StdHeight = 80
20 // Maximum absolute skew factor of a single digit.
21 maxSkew = 0.7
22 // Number of background circles.
23 circleCount = 20
24)
25
26type Image struct {
27 *image.Paletted
28 numWidth int
29 numHeight int
30 dotSize int
31}
32
33func randomPalette() image.PalettedColorModel {
34 p := make([]image.Color, circleCount+1)
35 // Transparent color.
36 // TODO(dchest). Currently it's white, not transparent, because PNG
37 // encoder doesn't support paletted images with alpha channel.
38 // Submitted CL: http://codereview.appspot.com/4432078 Change alpha to
39 // 0x00 once it's accepted.
40 p[0] = image.RGBAColor{0xFF, 0xFF, 0xFF, 0xFF}
41 // Primary color.
42 prim := image.RGBAColor{
43 uint8(rand.Intn(129)),
44 uint8(rand.Intn(129)),
45 uint8(rand.Intn(129)),
46 0xFF,
47 }
48 p[1] = prim
49 // Circle colors.
50 for i := 2; i <= circleCount; i++ {
51 p[i] = randomBrightness(prim, 255)
52 }
53 return p
54}
55
56// NewImage returns a new captcha image of the given width and height with the
57// given digits, where each digit must be in range 0-9.
58func NewImage(digits []byte, width, height int) *Image {
59 m := new(Image)
60 m.Paletted = image.NewPaletted(width, height, randomPalette())
61 m.calculateSizes(width, height, len(digits))
62 // Randomly position captcha inside the image.
63 maxx := width - (m.numWidth+m.dotSize)*len(digits) - m.dotSize
64 maxy := height - m.numHeight - m.dotSize*2
65 var border int
66 if width > height {
67 border = height / 5
68 } else {
69 border = width / 5
70 }
71 x := rnd(border, maxx-border)
72 y := rnd(border, maxy-border)
73 // Draw digits.
74 for _, n := range digits {
75 m.drawDigit(font[n], x, y)
76 x += m.numWidth + m.dotSize
77 }
78 // Draw strike-through line.
79 m.strikeThrough()
80 // Apply wave distortion.
81 m.distort(rndf(5, 10), rndf(100, 200))
82 // Fill image with random circles.
83 m.fillWithCircles(circleCount, m.dotSize)
84 return m
85}
86
87// BUG(dchest): While Image conforms to io.WriterTo interface, its WriteTo
88// method returns 0 instead of the actual bytes written because png.Encode
89// doesn't report this.
90
91// WriteTo writes captcha image in PNG format into the given writer.
92func (m *Image) WriteTo(w io.Writer) (int64, os.Error) {
93 return 0, png.Encode(w, m.Paletted)
94}
95
96func (m *Image) calculateSizes(width, height, ncount int) {
97 // Goal: fit all digits inside the image.
98 var border int
99 if width > height {
100 border = height / 4
101 } else {
102 border = width / 4
103 }
104 // Convert everything to floats for calculations.
105 w := float64(width - border*2)
106 h := float64(height - border*2)
107 // fw takes into account 1-dot spacing between digits.
108 fw := float64(fontWidth + 1)
109 fh := float64(fontHeight)
110 nc := float64(ncount)
111 // Calculate the width of a single digit taking into account only the
112 // width of the image.
113 nw := w / nc
114 // Calculate the height of a digit from this width.
115 nh := nw * fh / fw
116 // Digit too high?
117 if nh > h {
118 // Fit digits based on height.
119 nh = h
120 nw = fw / fh * nh
121 }
122 // Calculate dot size.
123 m.dotSize = int(nh / fh)
124 // Save everything, making the actual width smaller by 1 dot to account
125 // for spacing between digits.
126 m.numWidth = int(nw) - m.dotSize
127 m.numHeight = int(nh)
128}
129
130func (m *Image) drawHorizLine(fromX, toX, y int, colorIdx uint8) {
131 for x := fromX; x <= toX; x++ {
132 m.SetColorIndex(x, y, colorIdx)
133 }
134}
135
136func (m *Image) drawCircle(x, y, radius int, colorIdx uint8) {
137 f := 1 - radius
138 dfx := 1
139 dfy := -2 * radius
140 xo := 0
141 yo := radius
142
143 m.SetColorIndex(x, y+radius, colorIdx)
144 m.SetColorIndex(x, y-radius, colorIdx)
145 m.drawHorizLine(x-radius, x+radius, y, colorIdx)
146
147 for xo < yo {
148 if f >= 0 {
149 yo--
150 dfy += 2
151 f += dfy
152 }
153 xo++
154 dfx += 2
155 f += dfx
156 m.drawHorizLine(x-xo, x+xo, y+yo, colorIdx)
157 m.drawHorizLine(x-xo, x+xo, y-yo, colorIdx)
158 m.drawHorizLine(x-yo, x+yo, y+xo, colorIdx)
159 m.drawHorizLine(x-yo, x+yo, y-xo, colorIdx)
160 }
161}
162
163func (m *Image) fillWithCircles(n, maxradius int) {
164 maxx := m.Bounds().Max.X
165 maxy := m.Bounds().Max.Y
166 for i := 0; i < n; i++ {
167 colorIdx := uint8(rnd(1, circleCount-1))
168 r := rnd(1, maxradius)
169 m.drawCircle(rnd(r, maxx-r), rnd(r, maxy-r), r, colorIdx)
170 }
171}
172
173func (m *Image) strikeThrough() {
174 maxx := m.Bounds().Max.X
175 maxy := m.Bounds().Max.Y
176 y := rnd(maxy/3, maxy-maxy/3)
177 amplitude := rndf(5, 20)
178 period := rndf(80, 180)
179 dx := 2.0 * math.Pi / period
180 for x := 0; x < maxx; x++ {
181 xo := amplitude * math.Cos(float64(y)*dx)
182 yo := amplitude * math.Sin(float64(x)*dx)
183 for yn := 0; yn < m.dotSize; yn++ {
184 r := rnd(0, m.dotSize)
185 m.drawCircle(x+int(xo), y+int(yo)+(yn*m.dotSize), r/2, 1)
186 }
187 }
188}
189
190func (m *Image) drawDigit(digit []byte, x, y int) {
191 skf := rndf(-maxSkew, maxSkew)
192 xs := float64(x)
193 r := m.dotSize / 2
194 y += rnd(-r, r)
195 for yo := 0; yo < fontHeight; yo++ {
196 for xo := 0; xo < fontWidth; xo++ {
197 if digit[yo*fontWidth+xo] != blackChar {
198 continue
199 }
200 m.drawCircle(x+xo*m.dotSize, y+yo*m.dotSize, r, 1)
201 }
202 xs += skf
203 x = int(xs)
204 }
205}
206
207func (m *Image) distort(amplude float64, period float64) {
208 w := m.Bounds().Max.X
209 h := m.Bounds().Max.Y
210
211 oldm := m.Paletted
212 newm := image.NewPaletted(w, h, oldm.Palette)
213
214 dx := 2.0 * math.Pi / period
215 for x := 0; x < w; x++ {
216 for y := 0; y < h; y++ {
217 xo := amplude * math.Sin(float64(y)*dx)
218 yo := amplude * math.Cos(float64(x)*dx)
219 newm.SetColorIndex(x, y, oldm.ColorIndexAt(x+int(xo), y+int(yo)))
220 }
221 }
222 m.Paletted = newm
223}
224
225func randomBrightness(c image.RGBAColor, max uint8) image.RGBAColor {
226 minc := min3(c.R, c.G, c.B)
227 maxc := max3(c.R, c.G, c.B)
228 if maxc > max {
229 return c
230 }
231 n := rand.Intn(int(max-maxc)) - int(minc)
232 return image.RGBAColor{
233 uint8(int(c.R) + n),
234 uint8(int(c.G) + n),
235 uint8(int(c.B) + n),
236 uint8(c.A),
237 }
238}
239
240func min3(x, y, z uint8) (m uint8) {
241 m = x
242 if y < m {
243 m = y
244 }
245 if z < m {
246 m = z
247 }
248 return
249}
250
251func max3(x, y, z uint8) (m uint8) {
252 m = x
253 if y > m {
254 m = y
255 }
256 if z > m {
257 m = z
258 }
259 return
260}